/**
 * Copyright (c) 2009 Noel Ang
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
package com.lunarshades.samsara.logging;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import com.lunarshades.samsara.Raider;
import com.lunarshades.samsara.ui.actions.ContestWinEvent;
import com.lunarshades.samsara.ui.actions.KarmaAddEvent;
import com.lunarshades.samsara.ui.actions.KarmaChangeEvent;
import com.lunarshades.samsara.ui.actions.KarmaStatusEvent;
import com.lunarshades.samsara.ui.actions.KarmaSubtractEvent;

/**
 * A Logger implementation that tracks karma changes.
 *
 * @author Noel Ang, sparta.variable@gmail.com
 */
public class KarmaChangeLogger<E extends Raider> extends Logger {

public KarmaChangeLogger() {
    mTracking = new LinkedHashMap<E, List<KarmaChangeEntry>>();
    mIsDisposed = false;
    mMethods = new HashMap<Class, Method>();
    initMethods();
}

private void initMethods() {
    mMethods.clear();
    try {
        mMethods.put(KarmaStatusEvent.class,
                this.getClass().getDeclaredMethod("log", KarmaStatusEvent.class
                )
        );
        mMethods.put(KarmaAddEvent.class,
                this.getClass().getDeclaredMethod("log", KarmaAddEvent.class
                )
        );
        mMethods.put(KarmaSubtractEvent.class,
                this.getClass().getDeclaredMethod("log",
                        KarmaSubtractEvent.class
                )
        );
        mMethods.put(ContestWinEvent.class,
                this.getClass().getDeclaredMethod("log", ContestWinEvent.class
                )
        );
    } catch (NoSuchMethodException e) {
        e.printStackTrace(System.err);
        throw new RuntimeException(e);
    }
}

private List<KarmaChangeEntry> startHistory(KarmaChangeEvent<E> e) {
    synchronized (mTracking) {
        List<KarmaChangeEntry> history = new LinkedList<KarmaChangeEntry>();
        E raider = e.subject();
        if (e instanceof KarmaStatusEvent) {
            history.add(new KarmaChangeEntry((KarmaStatusEvent<? extends Raider>) e));
        } else {
            KarmaStatusEvent<Raider> genesis = new KarmaStatusEvent<Raider>(raider);
            history.add(new KarmaChangeEntry(genesis));
            reconcileIntoHistory(history, e);
        }
        mTracking.put(raider, history);
        return history;
    }
}

private void reconcileIntoHistory(List<KarmaChangeEntry> history,
                                  KarmaChangeEvent<E> e
) {
    assert history.size() > 0;
    final KarmaChangeEntry recent = history.get(history.size() - 1);
    final E raider = e.subject();
    int diff = raider.karma().intValue() - recent.newValue;
    if (diff > 0) {
        KarmaAddEvent<E> addEvent = new KarmaAddEvent<E>(raider, diff, null);
        history.add(new KarmaChangeEntry(addEvent) {
            @Override
            protected String string() {
                StringBuffer buffer = new StringBuffer();
                buffer.append(recent.newValue).append(" +?> ");
                buffer.append(raider.karma().intValue());
                return buffer.toString();
            }
        });
    } else if (diff < 0){
        KarmaSubtractEvent<E> subEvent =
                new KarmaSubtractEvent<E>(raider, 0 - diff, null);
        history.add(new KarmaChangeEntry(subEvent) {
            @Override
            protected String string() {
                StringBuffer buffer = new StringBuffer();
                buffer.append(recent.newValue).append(" -?> ");
                buffer.append(raider.karma().intValue());
                return buffer.toString();
            }
        });
    }
}

private void log(KarmaStatusEvent<E> e) {
    assert e != null;
    E raider = e.subject();
    synchronized (mTracking) {
        List<KarmaChangeEntry> changes = mTracking.get(raider);
        if (changes == null) {
            // No history, create one. This event represents initial state.
            startHistory(e);
        } else {
            // Raider existed before this call, and already has a history of
            // karma changes. That history was initiated by a KarmaStatusEvent.
            //
            // If this event cannot be reconciled with the most recent history
            // event, add it as an anachronism.  Otherwise, ignore it since
            // it is superfluous.
            reconcileIntoHistory(changes, e);
        }
    }
}

private void log(KarmaAddEvent<E> e) {
    assert e != null;
    E raider = e.subject();
    synchronized (mTracking) {
        List<KarmaChangeEntry> changes = mTracking.get(raider);
        if (changes == null) {
            changes = startHistory(e);
        }
        changes.add(new KarmaChangeEntry(e));
    }
}

private void log(KarmaSubtractEvent<E> e) {
    assert e != null;
    E raider = e.subject();
    synchronized (mTracking) {
        List<KarmaChangeEntry> changes = mTracking.get(raider);
        if (changes == null) {
            changes = startHistory(e);
        }
        changes.add(new KarmaChangeEntry(e));
    }
}

private void log(ContestWinEvent<E> e) {
    assert e != null;
    E raider = e.subject();
    synchronized (mTracking) {
        List<KarmaChangeEntry> changes = mTracking.get(raider);
        if (changes == null) {
            changes = startHistory(e);
        }
        changes.add(new KarmaChangeEntry(e));
    }
}

public Loggable[] dumpLog() {
    List<Loggable> log = new LinkedList<Loggable>();

    synchronized (mTracking) {
        // To impose Raider-based ordering on the listing
        SortedSet<E> raiders = new TreeSet<E>(mTracking.keySet());

        final int bufferSize = (mTracking.size() * 100);
        int count = 0;

        for (final E raider : raiders) {
            final int index = ++count;
            log.add(new Loggable() {
                public Object message() {
                    List<KarmaChangeEntry> history = mTracking.get(raider);
                    StringBuffer buffer = new StringBuffer(bufferSize);
                    buffer.append(index)
                            .append(". ")
                            .append(raider)
                            .append(":");
                    for (KarmaChangeEntry changeEntry : history) {
                        buffer.append(changeEntry);
                    }
                    return buffer.toString();
                }
            }
            );
        }

        if (!raiders.isEmpty()) {
            log.add(0, BLANK_LOG);
            log.add(1, START_LOG);
        }
        return log.toArray(new Loggable[log.size()]);
    }
}

/**
 * Consume the specified event.
 *
 * @param event A Loggable object.
 *
 * @throws IllegalStateException if {@link #dispose()} has been previously
 *                               invoked.
 */
public void log(Loggable event) {
    if (!mIsDisposed && event instanceof KarmaChangeEvent) {
        KarmaChangeEvent karmaEvent = (KarmaChangeEvent) event;
        Method method = mMethods.get(karmaEvent.getClass());
        assert method != null;
        try {
            method.invoke(this, karmaEvent);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}

/**
 * Decides if the specified event is fit for consumption.
 *
 * @param event The event whose suitability is to be determined.
 *
 * @return True if the specified event would cause a subsequent call to the
 *         implementation's {@link #log(Loggable)} to be meaningful; false
 *         otherwise.
 */
public boolean accept(Loggable event) {
    return (event instanceof KarmaChangeEvent);
}

/**
 * Discard the logger instance. This method provides a chance for an
 * implementation to free up resources. Once this method is called the object
 * should no longer be used; its behaviour after that point is undefined.
 */
public void dispose() {
    synchronized (mMethods) {
        mMethods.clear();
        synchronized (mTracking) {
            for (List<KarmaChangeEntry> history : mTracking.values()) {
                history.clear();
            }
            mTracking.clear();
        }
    }
    mIsDisposed = true;
}

public static class KarmaChangeEntry {

    public KarmaChangeEntry(ContestWinEvent<? extends Raider> e) {
        newValue = e.subject().karma().intValue();
        oldValue = newValue + e.magnitude();
        deltaOp = e.isContested() ? ">" : "@>";
        STRING_FORM = string();
    }

    public KarmaChangeEntry(KarmaAddEvent<? extends Raider> e) {
        newValue = e.subject().karma().intValue();
        oldValue = newValue - e.magnitude();
        deltaOp = "+>";
        STRING_FORM = string();
    }

    public KarmaChangeEntry(KarmaSubtractEvent<? extends Raider> e) {
        newValue = e.subject().karma().intValue();
        oldValue = newValue + e.magnitude();
        deltaOp = ">";
        STRING_FORM = string();
    }

    public KarmaChangeEntry(KarmaStatusEvent<? extends Raider> e) {
        newValue = e.subject().karma().intValue();
        oldValue = newValue;
        deltaOp = "";
        STRING_FORM = string();
    }

    protected String string() {
        StringBuffer buffer = new StringBuffer(" ");
        if (!"".equals(deltaOp)) {
            buffer.append(deltaOp).append(" ");
        }
        buffer.append(newValue);
        return buffer.toString();
    }

    @Override
    public String toString() {
        return STRING_FORM;
    }

    private final String STRING_FORM;
    private final String deltaOp;
    public final int oldValue;
    public final int newValue;
}

private static final Loggable BLANK_LOG = new Loggable() {
    public Object message() {
        return "";
    }
};
private static final Loggable START_LOG = new Loggable() {
    public Object message() {
        return "Karma Activity View:";
    }
};
private final Map<Class, Method> mMethods;
private final Map<E, List<KarmaChangeEntry>> mTracking;
private volatile boolean mIsDisposed;
}
